// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Text;
using System.Collections.Generic;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;

using Anculus.Core;

namespace Galaxium.Protocol.Xmpp.Library.Sasl
{
	// SASL DIGEST-MD5 authentication helper (RFC2831)
	public class DigestMD5: Mechanism
	{
		public DigestMD5 (Func<Element, Element> method)
			:base (method)
		{
		}

		#region Private methods
		
		private enum DecodeState {Key, Value, Quote};
		
		private static Dictionary<string, string> DecodeChallenge (string challenge)
		{
			string text = Cryptography.Base64Decode (challenge);
			Dictionary<string,string> result = new Dictionary<string, string> ();
			
			DecodeState state = DecodeState.Key;
			
			StringBuilder key = new StringBuilder ();
			StringBuilder val = new StringBuilder ();
				
			foreach (char ch in text) {
				switch (state) {
				case DecodeState.Key:
					if (ch == '=') state = DecodeState.Value;
					else key.Append (ch);
					break;
				case DecodeState.Value:
					if (ch == ',') {
						result[key.ToString ()] = val.ToString ();
						key = new StringBuilder ();
						val = new StringBuilder ();
						state = DecodeState.Key;
					}
					else if (ch == '"' && val.Length == 0) state = DecodeState.Quote;
					else val.Append (ch);
					break;
				case DecodeState.Quote:
					if (ch == '"') state = DecodeState.Value;
					else val.Append (ch);
					break;
				}
			}
			
			if (key.Length > 0)
				result [key.ToString ()] = val.ToString ();

			Log.Debug ("DIGEST-MD5 challenge: " + text);
			
			return result;
		}
		
		// Function from RFC2831
		private static byte[] h (string s)
		{
			return Cryptography.Md5Hash (s);
		}

		// Function from RFC2831
		private static string hh (byte[] bytes)
		{
			return Cryptography.Md5HexHash (bytes);
		}
		
		private static string hh (string s)
		{
			return Cryptography.Md5HexHash (s);
		}

		private static string Quot (string s)
		{
			return '"' + s + '"';
		}

		private static void ArrayAdd (ref byte[] arr1, byte[] arr2)
		{		
			int index = arr1.Length;
			Array.Resize (ref arr1, index + arr2.Length);
			arr2.CopyTo (arr1, index);
		}
		
		// Calculate value for the response field
		private static string ResponseValue (string username, string realm, string digest_uri,
		                                     string passwd, string nonce, string cnonce, string qop)
		{
			byte[] arr = h (username + ':' + realm + ':' + passwd);
			ArrayAdd (ref arr, Encoding.UTF8.GetBytes (':' + nonce + ':' + cnonce));			

			string[] response_fields = new string[] {
				hh (arr), nonce, "00000001", cnonce, qop, hh ("AUTHENTICATE:" + digest_uri)
			};
			
			return hh (String.Join (":", response_fields));
		}

		#endregion
		
		// * Send a response
		// * Wait for the server's challenge (which isn't checked)
		// * Send a blind response to the server's challenge
		public override void Auth (JabberID jid, string password)
		{
			var reply = Send (GenerateAuth ("DIGEST-MD5", null));
			
			Dictionary<string, string> challenge;
			if (reply.Name == "challenge" && reply.Namespace == Namespaces.Sasl)
				challenge = DecodeChallenge (reply.GetText ());
			else throw new SaslError (reply);
			
			string nonce = challenge ["nonce"];
			string realm = challenge.ContainsKey ("realm") ? challenge ["realm"] : jid.Domain;
			string cnonce = GenerateNonce ();
			string digest_uri = String.Concat ("xmpp/", jid.Domain);
			
			Dictionary<string,string> response = new Dictionary<string,string>();
			
			response ["nonce"]      = Quot (nonce);
			response ["username"]   = Quot (jid.Node);
			response ["realm"]      = Quot (realm);
			response ["cnonce"]     = Quot (cnonce);
			response ["digest-uri"] = Quot (digest_uri);
			response ["nc"]         = "00000001";
			response ["qop"]        = "auth";
			response ["charset"]    = "utf-8";
			response ["response"]   = ResponseValue (jid.Node, jid.Domain, digest_uri, password,
			                                         nonce, cnonce, response ["qop"]);
			
			StringBuilder response_text = new StringBuilder ();
			
			foreach (string key in response.Keys)
				response_text.Append (String.Concat (key, '=', response [key], ','));
			response_text.Remove (response_text.Length - 1, 1); // delete last comma

			Log.Debug ("sasl", "DIGEST-MD5 response: " + response_text.ToString ());

			Element r = new Element ("response", Namespaces.Sasl);
			r.SetText (Cryptography.Base64Encode (response_text.ToString ()));
			
			reply = Send (r);
			
			if (reply.Name == "success") return;
			if (reply.Name != "challenge") throw new SaslError (reply);
			
			// TODO: check the challenge from server

			r.SetText (null);
			reply = Send (r);
			
			if (reply.Name != "success") throw new SaslError (reply);
		}
	}
}
